#!/usr/bin/env python3
"""
GitLab批量分支创建脚本
用于按前缀匹配企业GitLab项目并批量创建指定版本分支
"""

import os
import sys
import json
import argparse
import logging
import fnmatch
from typing import List, Dict, Optional
import requests
from urllib.parse import quote


class GitLabBatchBranchCreator:
    def __init__(self, gitlab_url: str, access_token: str):
        self.gitlab_url = gitlab_url.rstrip('/')
        self.access_token = access_token
        self.headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
        self.session = requests.Session()
        self.session.headers.update(self.headers)
        
        # 设置日志
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('batch_branch_creator.log'),
                logging.StreamHandler(sys.stdout)
            ]
        )
        self.logger = logging.getLogger(__name__)

    def search_projects(self, prefix: Optional[str] = None, groups: Optional[List[str]] = None, exclude_patterns: Optional[List[str]] = None) -> List[Dict]:
        """搜索项目，支持按前缀或分组搜索，以及排除模式"""
        if not prefix and not groups:
            raise ValueError("必须指定项目前缀或分组中的至少一个")
        
        if prefix and groups:
            self.logger.info(f"搜索前缀为 '{prefix}' 的项目，限制在分组: {', '.join(groups)}")
        elif prefix:
            self.logger.info(f"全局搜索前缀为 '{prefix}' 的项目...")
        elif groups:
            self.logger.info(f"获取分组中的所有项目: {', '.join(groups)}")
        
        all_projects = []
        valid_groups_found = False
        
        if groups:
            # 如果指定了分组，遍历每个分组获取项目
            for group in groups:
                if prefix:
                    # 在分组中按前缀搜索
                    group_projects = self._search_projects_in_group(prefix, group)
                else:
                    # 获取分组中的所有项目
                    group_projects = self._get_all_projects_in_group(group)
                
                if group_projects:  # 只有找到项目才认为分组有效
                    all_projects.extend(group_projects)
                    valid_groups_found = True
            
            # 如果没有在任何指定分组中找到项目，且有前缀，则回退到全局搜索
            if not valid_groups_found and prefix:
                self.logger.info("在指定分组中未找到项目，回退到全局搜索...")
                all_projects = self._search_projects_global(prefix)
        elif prefix:
            # 仅指定了前缀，全局搜索
            all_projects = self._search_projects_global(prefix)
        
        # 去重（基于项目ID）
        unique_projects = {}
        for project in all_projects:
            unique_projects[project['id']] = project
        
        projects = list(unique_projects.values())
        
        # 应用排除模式
        if exclude_patterns:
            projects = self._filter_excluded_projects(projects, exclude_patterns)
        
        self.logger.info(f"找到 {len(projects)} 个匹配的项目")
        return projects
    
    def _search_projects_global(self, prefix: str) -> List[Dict]:
        """全局搜索项目"""
        projects = []
        page = 1
        per_page = 100
        
        while True:
            params = {
                'search': prefix,
                'page': page,
                'per_page': per_page,
                'simple': True
            }
                
            try:
                url = f"{self.gitlab_url}/api/v4/projects"
                response = self.session.get(url, params=params)
                response.raise_for_status()
                
                page_projects = response.json()
                if not page_projects:
                    break
                    
                # 过滤出真正匹配前缀的项目
                filtered_projects = [
                    p for p in page_projects 
                    if p['name'].startswith(prefix) or p['path'].startswith(prefix)
                ]
                
                projects.extend(filtered_projects)
                page += 1
                
            except requests.exceptions.RequestException as e:
                self.logger.error(f"全局搜索项目时出错: {e}")
                break
                
        return projects
    
    def _search_projects_in_group(self, prefix: str, group: str) -> List[Dict]:
        """在指定分组中按前缀搜索项目"""
        return self._get_projects_from_group(group, prefix)
    
    def _get_all_projects_in_group(self, group: str) -> List[Dict]:
        """获取指定分组中的所有项目"""
        return self._get_projects_from_group(group, None)
    
    def _get_projects_from_group(self, group: str, prefix: Optional[str] = None) -> List[Dict]:
        """从指定分组获取项目，可选按前缀过滤"""
        projects = []
        page = 1
        per_page = 100
        
        try:
            # 首先获取分组ID
            group_url = f"{self.gitlab_url}/api/v4/groups/{quote(group, safe='')}"
            group_response = self.session.get(group_url)
            
            if group_response.status_code == 404:
                self.logger.warning(f"分组 '{group}' 不存在")
                return []
            
            group_response.raise_for_status()
            group_info = group_response.json()
            group_id = group_info['id']
            
            if prefix:
                self.logger.debug(f"在分组 '{group}' (ID: {group_id}) 中按前缀 '{prefix}' 搜索项目")
            else:
                self.logger.debug(f"获取分组 '{group}' (ID: {group_id}) 中的所有项目")
            
            # 获取分组内的项目
            while True:
                params = {
                    'page': page,
                    'per_page': per_page,
                    'simple': True,
                    'include_subgroups': True  # 包含子分组
                }
                
                url = f"{self.gitlab_url}/api/v4/groups/{group_id}/projects"
                response = self.session.get(url, params=params)
                response.raise_for_status()
                
                page_projects = response.json()
                if not page_projects:
                    break
                
                if prefix:
                    # 过滤出匹配前缀的项目
                    filtered_projects = [
                        p for p in page_projects 
                        if p['name'].startswith(prefix) or p['path'].startswith(prefix)
                    ]
                    projects.extend(filtered_projects)
                else:
                    # 所有项目
                    projects.extend(page_projects)
                
                page += 1
                
        except requests.exceptions.RequestException as e:
            self.logger.error(f"在分组 '{group}' 中获取项目时出错: {e}")
            
        if prefix:
            self.logger.debug(f"在分组 '{group}' 中找到 {len(projects)} 个匹配 '{prefix}' 的项目")
        else:
            self.logger.debug(f"在分组 '{group}' 中找到 {len(projects)} 个项目")
        return projects
    
    def _filter_excluded_projects(self, projects: List[Dict], exclude_patterns: List[str]) -> List[Dict]:
        """根据排除模式过滤项目"""
        if not exclude_patterns:
            return projects
        
        initial_count = len(projects)
        filtered_projects = []
        excluded_projects = []
        
        for project in projects:
            project_name = project['name']
            project_path = project['path']
            project_full_path = project['path_with_namespace']
            
            # 检查项目是否匹配任何排除模式
            is_excluded = False
            matched_pattern = None
            
            for pattern in exclude_patterns:
                # 检查项目名称、路径或完整路径是否匹配模式
                if (fnmatch.fnmatch(project_name.lower(), pattern.lower()) or
                    fnmatch.fnmatch(project_path.lower(), pattern.lower()) or
                    fnmatch.fnmatch(project_full_path.lower(), pattern.lower())):
                    is_excluded = True
                    matched_pattern = pattern
                    break
            
            if is_excluded:
                excluded_projects.append({
                    'name': project_name,
                    'path': project_full_path,
                    'pattern': matched_pattern
                })
            else:
                filtered_projects.append(project)
        
        # 记录排除的项目
        if excluded_projects:
            self.logger.info(f"根据排除模式过滤掉 {len(excluded_projects)} 个项目:")
            for excluded in excluded_projects:
                self.logger.info(f"  排除: {excluded['name']} ({excluded['path']}) - 匹配模式: {excluded['pattern']}")
        
        self.logger.debug(f"排除前: {initial_count} 个项目，排除后: {len(filtered_projects)} 个项目")
        return filtered_projects

    def get_project_branches(self, project_id: int) -> List[Dict]:
        """获取项目的所有分支"""
        branches = []
        page = 1
        per_page = 100
        
        try:
            while True:
                params = {
                    'page': page,
                    'per_page': per_page
                }
                
                url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/branches"
                response = self.session.get(url, params=params)
                response.raise_for_status()
                
                page_branches = response.json()
                if not page_branches:
                    break
                    
                branches.extend(page_branches)
                self.logger.debug(f"第 {page} 页获取到 {len(page_branches)} 个分支")
                page += 1
                
            self.logger.debug(f"项目 {project_id} 总共获取到 {len(branches)} 个分支")
            return branches
            
        except requests.exceptions.RequestException as e:
            self.logger.error(f"获取项目 {project_id} 分支信息失败: {e}")
            return []

    def create_branch(self, project_id: int, branch_name: str, ref: str = 'main') -> bool:
        """在指定项目中创建分支"""
        try:
            url = f"{self.gitlab_url}/api/v4/projects/{project_id}/repository/branches"
            data = {
                'branch': branch_name,
                'ref': ref
            }
            
            response = self.session.post(url, json=data)
            
            if response.status_code == 201:
                self.logger.info(f"成功创建分支 '{branch_name}' (项目ID: {project_id})")
                return True
            elif response.status_code == 400 and 'already exists' in response.text:
                self.logger.warning(f"分支 '{branch_name}' 已存在 (项目ID: {project_id})")
                return True
            else:
                self.logger.error(f"创建分支失败 (项目ID: {project_id}): {response.text}")
                return False
                
        except requests.exceptions.RequestException as e:
            self.logger.error(f"创建分支时出错 (项目ID: {project_id}): {e}")
            return False

    def batch_create_branches(self, branch_name: str, source_branch: str = 'main', 
                            prefix: Optional[str] = None, groups: Optional[List[str]] = None, 
                            branch_prefix: Optional[str] = None, exclude_patterns: Optional[List[str]] = None,
                            dry_run: bool = False) -> Dict:
        """批量创建分支"""
        # 如果指定了分支前缀，则在分支名前添加前缀
        final_branch_name = f"{branch_prefix}{branch_name}" if branch_prefix else branch_name
        
        self.logger.info(f"开始批量创建分支 '{final_branch_name}' (源分支: {source_branch})")
        if branch_prefix:
            self.logger.info(f"使用分支前缀: '{branch_prefix}'")
        
        if dry_run:
            self.logger.info("*** 试运行模式 - 不会实际创建分支 ***")
        
        if exclude_patterns:
            self.logger.info(f"排除模式: {', '.join(exclude_patterns)}")
        
        projects = self.search_projects(prefix, groups, exclude_patterns)
        
        if not projects:
            self.logger.warning("没有找到匹配的项目")
            return {'success': 0, 'failed': 0, 'total': 0}
        
        results = {
            'success': 0,
            'failed': 0,
            'total': len(projects),
            'details': []
        }
        
        for project in projects:
            project_id = project['id']
            project_name = project['name']
            project_path = project['path_with_namespace']
            
            self.logger.info(f"处理项目: {project_name} ({project_path})")
            
            # 检查源分支是否存在
            branches = self.get_project_branches(project_id)
            branch_names = [b['name'] for b in branches]
            
            if source_branch not in branch_names:
                self.logger.warning(f"项目 {project_name} 中不存在源分支 '{source_branch}'，跳过")
                self.logger.info(f"可用分支: {', '.join(branch_names)}")
                results['failed'] += 1
                results['details'].append({
                    'project': project_name,
                    'status': 'failed',
                    'reason': f"源分支 '{source_branch}' 不存在"
                })
                continue
            
            if not dry_run:
                success = self.create_branch(project_id, final_branch_name, source_branch)
                if success:
                    results['success'] += 1
                    results['details'].append({
                        'project': project_name,
                        'status': 'success',
                        'branch': final_branch_name
                    })
                else:
                    results['failed'] += 1
                    results['details'].append({
                        'project': project_name,
                        'status': 'failed',
                        'reason': '创建分支失败'
                    })
            else:
                self.logger.info(f"[试运行] 将在项目 {project_name} 中创建分支 '{final_branch_name}'")
                results['success'] += 1
                results['details'].append({
                    'project': project_name,
                    'status': 'dry_run',
                    'branch': final_branch_name
                })
        
        return results


def load_config(config_file: str) -> Dict:
    """加载配置文件"""
    try:
        with open(config_file, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"配置文件 {config_file} 不存在")
        return {}
    except json.JSONDecodeError as e:
        print(f"配置文件格式错误: {e}")
        return {}


def main():
    parser = argparse.ArgumentParser(description='GitLab批量分支创建工具')
    parser.add_argument('--gitlab-url', help='GitLab服务器URL')
    parser.add_argument('--token', help='GitLab访问令牌')
    parser.add_argument('--prefix', help='项目名称前缀')
    parser.add_argument('--branch', required=True, help='要创建的分支名称')
    parser.add_argument('--source-branch', help='源分支名称')
    parser.add_argument('--groups', nargs='+', help='GitLab分组名称（可指定多个）')
    parser.add_argument('--branch-prefix', help='分支名前缀')
    parser.add_argument('--exclude', nargs='+', help='排除项目的模式（支持通配符，可指定多个）')
    parser.add_argument('--dry-run', action='store_true', help='试运行模式，不实际创建分支')
    parser.add_argument('--config', help='配置文件路径')
    
    # 保留老参数兼容性
    parser.add_argument('--namespace', help='命名空间过滤 (已废弃，使用--groups替代)')
    
    args = parser.parse_args()
    
    # 如果提供了配置文件，从中加载配置
    config = {}
    if args.config:
        config = load_config(args.config)
    
    # 命令行参数优先于配置文件
    gitlab_url = args.gitlab_url or config.get('gitlab_url')
    token = args.token or config.get('access_token') or os.getenv('GITLAB_TOKEN')
    source_branch = args.source_branch or config.get('default_source_branch', 'main')
    branch_prefix = args.branch_prefix or config.get('default_branch_prefix')
    
    # 处理分组参数（支持多种格式）
    groups = None
    if args.groups:
        groups = args.groups
    elif config.get('default_groups'):
        groups = config.get('default_groups')
    elif args.namespace:  # 兼容老参数
        groups = [args.namespace]
    
    # 处理排除模式参数
    exclude_patterns = None
    if args.exclude:
        exclude_patterns = args.exclude
    elif config.get('default_exclude_patterns'):
        exclude_patterns = config.get('default_exclude_patterns')
    
    if not gitlab_url or not token:
        print("错误: 必须提供GitLab URL和访问令牌")
        sys.exit(1)
    
    # 验证参数：必须指定前缀或分组中的至少一个
    if not args.prefix and not groups:
        print("错误: 必须指定项目前缀(--prefix)或分组(--groups)中的至少一个")
        sys.exit(1)
    
    creator = GitLabBatchBranchCreator(gitlab_url, token)
    
    try:
        results = creator.batch_create_branches(
            branch_name=args.branch,
            source_branch=source_branch,
            prefix=args.prefix,
            groups=groups,
            branch_prefix=branch_prefix,
            exclude_patterns=exclude_patterns,
            dry_run=args.dry_run
        )
        
        print(f"\n批量创建分支完成:")
        print(f"总项目数: {results['total']}")
        print(f"成功: {results['success']}")
        print(f"失败: {results['failed']}")
        
        if results['failed'] > 0:
            print("\n失败详情:")
            for detail in results['details']:
                if detail['status'] == 'failed':
                    print(f"  - {detail['project']}: {detail['reason']}")
        
    except KeyboardInterrupt:
        print("\n操作被用户中断")
        sys.exit(1)
    except Exception as e:
        print(f"执行过程中出现错误: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()